/**
 * Copyright (C) 2011 Vinicius Vielmo Cogo, Marcelo Pasin, and the authors
 * indicated in the @author tags.
 * 
 * This file is part of DiversityAgent.
 * 
 * DiversityAgent is a free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 * 
 * DiversityAgent is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with DiversityAgent. If not, see <http://www.gnu.org/licenses/>.
 */
package diversity_agent;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Properties;

/**
 * This is the initial interaction class of the DivesityAgent component. The
 * agent is responsible to provide automatic diversity selection for distributed
 * systems using cloud computing.
 * 
 * @author Vinicius Vielmo Cogo (vielmo at lasige.di.fc.ul.pt)
 * @version 0.1-beta
 * @since 2011-11-18
 * 
 */
public class DiversityAgent
{
	private DataContainer		mDataContainer;
	private CloudFactory		mCloudFactory;
	private DiversityFactory	mDiversityFactory;

	// Library initialisation /////////////////////////////////////////////////
	/**
	 * Instantiate the DiversityAgent library.
	 */
	public DiversityAgent()
	{
		this.init("");
	}

	/**
	 * Instantiates the DiversityAgent library based on an existent state file.
	 * 
	 * @param theRecoveryStateFile
	 *            The path to a file containing a previously saved state.
	 */
	public DiversityAgent(String theRecoveryStateFile)
	{
		this.init(theRecoveryStateFile);
	}

	/**
	 * Initialises DiversityAgent properties.
	 * 
	 * @param theRecoveryStateFile
	 *            The path to a file containing a previously saved state or an
	 *            empty string.
	 */
	private void init(String theRecoveryStateFile)
	{
		if (theRecoveryStateFile.equals("")) {
			this.mDataContainer = null;
		} else {
			this.mDataContainer = readState(theRecoveryStateFile);
		}
		if (this.mDataContainer == null) {
			this.mDataContainer = new DataContainer();
		}
		this.mCloudFactory = new CloudFactory();
		this.mDiversityFactory = new DiversityFactory();
	}

	// CRUD Diversity //////////////////////////////////////////////////////////
	/**
	 * Register a diversity in current session.
	 * 
	 * @param theDiversity
	 *            The tag to the diversity requested.
	 * @return true if the diversity in question was registered.
	 */
	public boolean createDiversity(String theDiversity)
	{
		Diversity aDiversity = mDiversityFactory.getDiversity(theDiversity);
		if (aDiversity != null) {
			return mDataContainer.createDiversity(aDiversity);
		} else {
			return false;
		}
	}

	/**
	 * Register one or more diversities in the current session.
	 * 
	 * @param theDiversities
	 *            Group of diversities to be registered in the DataContainer.
	 * @return true if all diversities were registered.
	 */
	public boolean createDiversity(String theDiversities[])
	{
		boolean aAllSuceed = true;
		for (String aDiversity : theDiversities) {
			if (createDiversity(aDiversity) == false) {
				aAllSuceed = false;
			}
		}
		return aAllSuceed;
	}

	/**
	 * Remove a diversity from current session.
	 * 
	 * @param theDiversity
	 *            The tag to the diversity requested.
	 * @return true if the diversity in question was removed.
	 */
	public boolean deleteDiversity(String theDiversity)
	{
		Diversity aDiversity = mDiversityFactory.getDiversity(theDiversity);
		if (aDiversity != null) {
			return mDataContainer.deleteDiversity(aDiversity);
		} else {
			return false;
		}
	}

	/**
	 * Remove one or more diversities from the current session.
	 * 
	 * @param theDiversities
	 *            Group of diversities to be removed from the DataContainer.
	 * @return The successfulness of removing the diversities.
	 */
	public boolean deleteDiversity(String theDiversities[])
	{
		boolean aAllSuceed = true;
		for (String aDiversity : theDiversities) {
			if (deleteDiversity(aDiversity) == false) {
				aAllSuceed = false;
			}
		}
		return aAllSuceed;
	}

	// CRUD Cloud /////////////////////////////////////////////////////////////
	/**
	 * Register a cloud provider in the current session.
	 * 
	 * @param theCloudName
	 *            A identifier name for the cloud provider.
	 * @param theTool
	 *            The software used by the cloud provider and the tag that
	 *            define which cloud driver will be used.
	 * @param theType
	 *            The cloud deployment model (private, public or hybrid).
	 * @param theAddress
	 *            The IP address to the cloud provider's front-end.
	 * @param theUsername
	 *            The user registered on the cloud provider.
	 * @param thePassword
	 *            The password registered for the user.
	 * @return The successfulness of registering the cloud provider.
	 */
	public boolean createCloud(String theCloudName, String theTool,
			String theType, String theAddress, String theUsername,
			String thePassword)
	{
		// [TODO] Check if authentication succeed
		if (!mDataContainer.containsCloud(theCloudName)) {
			Cloud aCloud = mCloudFactory.getCloud(theCloudName, theTool,
					theType, theAddress, theUsername, thePassword);
			if (aCloud != null) {
				return mDataContainer.createCloud(aCloud);
			}
		} else {
			mDataContainer.unblockCloud(theCloudName);
		}
		return false;
	}

	/**
	 * Remove a cloud provider in the current session.
	 * 
	 * @param theCloudName
	 *            The identifier name registered to be removed.
	 * @return The successfulness of removing the cloud provider.
	 */
	public boolean deleteCloud(String theCloudName)
	{
		Cloud aCloud = mDataContainer.retrieveCloud(theCloudName);
		if (aCloud != null) {
			if (aCloud.countRunningVms() == 0) {
				this.deleteAllImagesFromCloud(theCloudName);
				return mDataContainer.deleteCloud(aCloud);
			}
			aCloud.block();
		}
		return false;
	}

	// CRUD Image /////////////////////////////////////////////////////////////
	/**
	 * Register a VM image as available in all clouds already registered.
	 * 
	 * @param theImageName
	 *            The VM image name to be registered.
	 * @return The successfulness of registering the VM image.
	 */
	public boolean createImage(String theImageName)
	{
		Image aImage = null;
		if (theImageName.split("#").length == 3) {
			aImage = new Image(theImageName);
		}
		// [TODO] Fetch image information automatically based on image name
		// Image aImage = new Image(theImageName);
		if (aImage != null) {
			for (Cloud aCloud : mDataContainer.retrieveClouds()) {
				aImage.addCloud(aCloud.getCloudName());
			}
		}
		return mDataContainer.createImage(aImage);
	}

	/**
	 * Register a VM image as available in some clouds already registered.
	 * 
	 * @param theImageName
	 *            The VM image name to be registered.
	 * @param theCloudNames
	 *            The clouds to be linked to the VM image.
	 * @return The successfulness of registering the VM image.
	 */
	public boolean createImage(String theImageName, String theCloudNames[])
	{
		// [TODO] Fetch image information automatically based on image name
		// Image aImage = new Image(theImageName);
		Image aImage = null;
		String[] aTmpStrings = theImageName.split("#");
		if (aTmpStrings.length == 3) {
			aImage = mDataContainer.retrieveImage(aTmpStrings[0]);
			if (aImage == null) {
				aImage = new Image(theImageName);
			}
			if (aImage != null) {
				for (String aCloud : theCloudNames) {
					aImage.addCloud(aCloud);
				}
				return mDataContainer.createImage(aImage);
			}
		}
		return false;
	}

	/**
	 * Unregister a image from a specific cloud provider.
	 * 
	 * @param theImageName
	 *            The VM image name to be unregistered.
	 * @param theCloudName
	 *            The cloud provider that is linked to the image.
	 * @return The successfulness of unregistering the VM image in that cloud.
	 */
	public boolean deleteImage(String theImageName, String theCloudName)
	{
		for (Image aImage : mDataContainer.retrieveImages()) {
			if (theImageName.equals(aImage.getName())) {
				aImage.deleteCloud(theCloudName);
				if (aImage.getNumClouds() == 0) {
					return mDataContainer.deleteImage(aImage.getName());
				} else {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Unregister a image from all registered cloud providers.
	 * 
	 * @param theImageName
	 *            The VM image name to be unregistered.
	 * @return The successfulness of unregistering the VM image in all clouds.
	 */
	public boolean deleteImage(String theImageName)
	{
		String[] aTmpStrings = theImageName.split("#");
		if (aTmpStrings.length == 3) {
			return mDataContainer.deleteImage(aTmpStrings[0]);
		} else {
			return mDataContainer.deleteImage(theImageName);
		}
	}

	/**
	 * Unregister all VM images from a specific cloud provider.
	 * 
	 * @param theCloudName
	 *            The cloud provider name to be unlinked to all images.
	 */
	public void deleteAllImagesFromCloud(String theCloudName)
	{
		for (Image aImage : mDataContainer.retrieveImages()) {
			aImage.deleteCloud(theCloudName);
			if (aImage.getNumClouds() == 0) {
				mDataContainer.deleteImage(aImage.getName());
			}
		}
	}

	// CRUD VirtualMachine ////////////////////////////////////////////////////
	/**
	 * Create a new Virtual Machine through the usage of all diversities.
	 * registered in current state.
	 * 
	 * @return The successfulness of creating the VM.
	 */
	public int createVm()
	{
		// Defining an empty property set
		Properties aProperties = new Properties();
		return createVm(aProperties);
	}

	/**
	 * Create a new Virtual Machine through the usage of all diversities and
	 * helped by some properties passed as argument.
	 * 
	 * @param theProperties
	 *            Some properties to be considered on the resource selection.
	 * @return The successfulness of creating the VM.
	 * @ensures The property set will not be overwritten.
	 */
	public int createVm(Properties theProperties)
	{
		// Ensure that the property set will not be overwritten
		Properties aProperties = (Properties) theProperties.clone();
		// Get contribution from diversities for properties set
		for (Diversity aDiversity : mDataContainer.retrieveDiversities()) {
			aProperties = aDiversity.getContribution(aProperties,
					mDataContainer);
		}
		// Instantiate the cloud provider
		Cloud aCloud = null;
		if (aProperties == null
				|| aProperties.getProperty("cloud.name") == null) {
			aCloud = mDataContainer.retrieveAnyCloud();
		} else {
			aCloud = mDataContainer.retrieveCloud(aProperties
					.getProperty("cloud.name"));
		}
		// Defining the VM ID on agent
		aProperties.setProperty("vm.id.agent", mDataContainer.getNextVmId()
				.toString());
		// Creating the new VirtualMachine
		VirtualMachine aVirtualMachine = aCloud.createVm(aProperties,
				mDataContainer);
		if (aVirtualMachine != null) {
			mDataContainer.createVm(aVirtualMachine);
			return aVirtualMachine.getIdOnAgent();
		} else {
			return -1;
		}
	}

	/**
	 * Read the instance of VirtualMachine indexed by the ID on agent.
	 * 
	 * @param theVmIdOnAgent
	 *            The identifier of the instance to be read.
	 * @return The VM instance indexed on agent by the identifier.
	 */
	public VirtualMachine readVm(int theVmIdOnAgent)
	{
		for (VirtualMachine aVm : mDataContainer.retrieveVms()) {
			if (theVmIdOnAgent == aVm.getIdOnAgent()) {
				return aVm;
			}
		}
		return null;
	}

	/**
	 * Read all VirtualMachine instances existent in the current state.
	 * 
	 * @return All currently registered VirtualMachine instances.
	 */
	public ArrayList<VirtualMachine> readVms()
	{
		return mDataContainer.retrieveVms();
	}

	/**
	 * Read the IP address of a specific VirtualMachine.
	 * 
	 * @param theVmIdOnAgent
	 *            The identifier of the instance to be read.
	 * @return The IP address of the specified VM.
	 */
	public String readVmIpAddress(int theVmIdOnAgent)
	{
		if (theVmIdOnAgent != -1) {
			VirtualMachine aVm = mDataContainer.retrieveVm(theVmIdOnAgent);
			return aVm.getIpAddress();
		} else {
			return null;
		}
	}

	/**
	 * Remove a Virtual Machine from the current group.
	 * 
	 * @param theVmIdOnAgent
	 *            The identifier of the instance to be removed.
	 * @return The successfulness of removing the VM.
	 */
	public boolean deleteVm(int theVmIdOnAgent)
	{
		VirtualMachine aVm = readVm(theVmIdOnAgent);
		if (aVm == null) {
			System.out
					.println("[ERROR     ] There is no VM with the given identifier.");
			System.out
					.println("[SOLUTION  ] Please try with another identifier.");
			return false;
		}
		Cloud aCloud = mDataContainer.retrieveCloud(aVm.getCloudName());
		if (aCloud.deleteVm(aVm.getIdOnCloud()) && mDataContainer.deleteVm(aVm)) {
			aCloud.unblock();
			return true;
		} else {
			return false;
		}
	}

	// Recovery State File ////////////////////////////////////////////////////
	/**
	 * Load an instance of DataContainer to the current session.
	 * 
	 * @param theFileName
	 *            Path to the file to be read in order to load the state.
	 * @return The successfulness of loading the state contained in the file.
	 */
	public boolean loadState(String theFileName)
	{
		DataContainer aDataContainer = readState(theFileName);
		if (aDataContainer != null) {
			this.mDataContainer = aDataContainer;
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Save the current instance of DataContainer in a file.
	 * 
	 * @param theFileName
	 *            Path to the file to be created or overwritten.
	 * @return The successfulness of saving the state in the file.
	 */
	public boolean saveState(String theFileName)
	{
		FileOutputStream aFileOutputStream = null;
		ObjectOutputStream aObjectOutputStream = null;
		try {
			aFileOutputStream = new FileOutputStream(theFileName);
			aObjectOutputStream = new ObjectOutputStream(aFileOutputStream);
			aObjectOutputStream.writeObject(mDataContainer);
			aObjectOutputStream.close();
			return true;
		} catch (Exception e) {
			System.out.println(e.getMessage());
			return false;
		}
	}

	/**
	 * Read a DataContainer from a backup file.
	 * 
	 * @param theFileName
	 *            Path to the file to be read in order to read the state.
	 * @return The instance of the DataContainer contained in the file.
	 */
	private DataContainer readState(String theFileName)
	{
		DataContainer aDataContainer;
		if (!theFileName.equals("")) {
			File aFile = new File(theFileName);
			if (aFile.exists()) {
				FileInputStream aFileInputStream = null;
				ObjectInputStream aObjectInputStream = null;
				try {
					aFileInputStream = new FileInputStream(theFileName);
					aObjectInputStream = new ObjectInputStream(aFileInputStream);
					aDataContainer = (DataContainer) aObjectInputStream
							.readObject();
					aObjectInputStream.close();
					return aDataContainer;
				} catch (Exception e) {
					System.out.println(e.getMessage());
				}
			}
		}
		return null;
	}

}
